/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

/**
 * Notes: For efficiency we handle String in a specialized way, in fact, a
 * java.lang.String is actually implemented as a native JavaScript String. Then
 * we just load up the prototype of the JavaScript String object with the
 * appropriate instance methods.
 */
package java.lang;

import com.google.gwt.core.client.JavaScriptObject;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Comparator;

/**
 * Intrinsic string class.
 *
 * TODO(jat): consider whether we want to support the following methods;
 *
 * <ul>
 * <li>deprecated methods dealing with bytes (I assume not since I can't see
 * much use for them)
 * <ul>
 * <li>String(byte[] ascii, int hibyte)
 * <li>String(byte[] ascii, int hibyte, int offset, int count)
 * <li>getBytes(int srcBegin, int srcEnd, byte[] dst, int dstBegin)
 * </ul>
 * <li>methods which in JS will essentially do nothing or be the same as other
 * methods
 * <ul>
 * <li>copyValueOf(char[] data)
 * <li>copyValueOf(char[] data, int offset, int count)
 * </ul>
 * <li>methods added in Java 1.6 (the issue is how will it impact users
 * building against Java 1.5)
 * <ul>
 * <li>isEmpty()
 * </ul>
 * <li>other methods which are not straightforward in JS
 * <ul>
 * <li>format(String format, Object... args)
 * </ul>
 * </ul>
 *
 * Also, in general, we need to improve our support of non-ASCII characters. The
 * problem is that correct support requires large tables, and we don't want to
 * make users who aren't going to use that pay for it. There are two ways to do
 * that:
 * <ol>
 * <li>construct the tables in such a way that if the corresponding method is
 * not called the table will be elided from the output.
 * <li>provide a deferred binding target selecting the level of compatibility
 * required. Those that only need ASCII (or perhaps a different relatively small
 * subset such as Latin1-5) will not pay for large tables, even if they do call
 * toLowercase(), for example.
 * </ol>
 *
 * Also, if we ever add multi-locale support, there are a number of other
 * methods such as toLowercase(Locale) we will want to consider supporting. This
 * is probably rare, but there will be some apps (such as a translation tool)
 * which cannot be written without this support.
 *
 * Another category of incomplete support is that we currently just use the JS
 * regex support, which is not exactly the same as Java. We should support Java
 * syntax by mapping it into equivalent JS patterns, or emulating them.
 */
public final class String implements Comparable<String>, CharSequence,
        Serializable {

    static final class HashCache {
        /**
         * The "old" cache; it will be dumped when front is full.
         */
        static JavaScriptObject back = JavaScriptObject.createObject();
        /**
         * Tracks the number of entries in front.
         */
        static int count = 0;
        /**
         * The "new" cache; it will become back when it becomes full.
         */
        static JavaScriptObject front = JavaScriptObject.createObject();
        /**
         * Pulled this number out of thin air.
         */
        static final int MAX_CACHE = 256;

        public static native int getHashCode(String str) /*-{
            // Accesses must to be prefixed with ':' to prevent conflict with built-in
            // JavaScript properties.
            var key = ':' + str;

            // Check the front store.
            var result = @java.lang.String.HashCache::front[key];
            if (result != null) {
                return result;
            }

            // Check the back store.
            result = @java.lang.String.HashCache::back[key];
            if (result == null) {
                // Compute the value.
                result = @java.lang.String.HashCache::compute(Ljava/lang/String;)(str);
            }
            // Increment can trigger the swap/flush; call after checking back but
            // before writing to front.
            @java.lang.String.HashCache::increment()();
            return @java.lang.String.HashCache::front[key] = result;
        }-*/;

        static int compute(String str) {
            int hashCode = 0;
            int n = str.length();
            int nBatch = n - 4;
            int i = 0;

            // Process batches of 4 characters at a time
            while (i < nBatch) {
                // Add the next 4 characters to the hash.
                // After every 4 characters, we force the result to fit into 32 bits
                // by doing a bitwise operation on it.
                hashCode = (str.charAt(i + 3)
                        + 31 * (str.charAt(i + 2)
                        + 31 * (str.charAt(i + 1)
                        + 31 * (str.charAt(i)
                        + 31 * hashCode)))) | 0;

                i += 4;
            }

            // Now process the leftovers
            while (i < n) {
                hashCode = hashCode * 31 + str.charAt(i++);
            }

            // TODO: make a JSNI call in case JDT gets smart about removing this
            // Do a final fitting to 32 bits
            return hashCode | 0;
        }

        static void increment() {
            if (count == MAX_CACHE) {
                back = front;
                front = JavaScriptObject.createObject();
                count = 0;
            }
            ++count;
        }
    }

    public static final Comparator<String> CASE_INSENSITIVE_ORDER = new Comparator<String>() {
        public int compare(String a, String b) {
            return a.compareToIgnoreCase(b);
        }
    };

    // names for standard character sets that are supported
    private static final String CHARSET_8859_1 = "ISO-8859-1";
    private static final String CHARSET_LATIN1 = "ISO-LATIN-1";
    private static final String CHARSET_UTF8 = "UTF-8";

    public static String copyValueOf(char[] v) {
        return valueOf(v);
    }

    public static String copyValueOf(char[] v, int offset, int count) {
        return valueOf(v, offset, count);
    }

    public static String valueOf(boolean x) {
        return "" + x;
    }

    public static native String valueOf(char x) /*-{
        return String.fromCharCode(x);
    }-*/;

    public static String valueOf(char x[], int offset, int count) {
        int end = offset + count;
        __checkBounds(x.length, offset, end);
        return __valueOf(x, offset, end);
    }

    public static native String valueOf(char[] x) /*-{
        // Trick: fromCharCode is a vararg method, so we can use apply() to pass the
        // entire input in one shot.
        return String.fromCharCode.apply(null, x);
    }-*/;

    public static String valueOf(double x) {
        return "" + x;
    }

    public static String valueOf(float x) {
        return "" + x;
    }

    public static String valueOf(int x) {
        return "" + x;
    }

    public static String valueOf(long x) {
        return "" + x;
    }

    public static String valueOf(Object x) {
        return "" + x;
    }

    // CHECKSTYLE_OFF: This class has special needs.

    /**
     * Checks that bounds are correct.
     *
     * @param legalCount the end of the legal range
     * @param start must be >= 0
     * @param end must be <= legalCount and must be >= start
     * @throw StringIndexOutOfBoundsException if the range is not legal
     * @skip
     */
    static void __checkBounds(int legalCount, int start, int end) {
        if (start < 0) {
            throw new StringIndexOutOfBoundsException(start);
        }
        if (end < start) {
            throw new StringIndexOutOfBoundsException(end - start);
        }
        if (end > legalCount) {
            throw new StringIndexOutOfBoundsException(end);
        }
    }

    /**
     * @skip
     */
    static String[] __createArray(int numElements) {
        return new String[numElements];
    }

    /**
     * This method converts Java-escaped dollar signs "\$" into JavaScript-escaped
     * dollar signs "$$", and removes all other lone backslashes, which serve as
     * escapes in Java but are passed through literally in JavaScript.
     *
     * @skip
     */
    static String __translateReplaceString(String replaceStr) {
        int pos = 0;
        while (0 <= (pos = replaceStr.indexOf("\\", pos))) {
            if (replaceStr.charAt(pos + 1) == '$') {
                replaceStr = replaceStr.substring(0, pos) + "$"
                        + replaceStr.substring(++pos);
            } else {
                replaceStr = replaceStr.substring(0, pos) + replaceStr.substring(++pos);
            }
        }
        return replaceStr;
    }

    static native String __valueOf(char x[], int start, int end) /*-{
        // Trick: fromCharCode is a vararg method, so we can use apply() to pass the
        // entire input in one shot.
        x = x.slice(start, end);
        return String.fromCharCode.apply(null, x);
    }-*/;

    /**
     * @skip
     */
    static String _String() {
        return "";
    }

    /**
     * @skip
     */
    static String _String(byte[] bytes) {
        return _String(bytes, 0, bytes.length);
    }

    /**
     * @skip
     */
    static String _String(byte[] bytes, int ofs, int len) {
        return utf8ToString(bytes, ofs, len);
    }

    /**
     * @skip
     */
    static String _String(byte[] bytes, int ofs, int len, String charset)
            throws UnsupportedEncodingException {
        if (CHARSET_UTF8.equals(charset)) {
            return utf8ToString(bytes, ofs, len);
        } else if (CHARSET_8859_1.equals(charset) || CHARSET_LATIN1.equals(charset)) {
            return latin1ToString(bytes, ofs, len);
        } else {
            throw new UnsupportedEncodingException("Charset " + charset
                    + " not supported");
        }
    }

    /**
     * @skip
     */
    static String _String(byte[] bytes, String charsetName)
            throws UnsupportedEncodingException {
        return _String(bytes, 0, bytes.length, charsetName);
    }

    /**
     * @skip
     */
    static String _String(char value[]) {
        return valueOf(value);
    }

    /**
     * @skip
     */
    static String _String(char value[], int offset, int count) {
        return valueOf(value, offset, count);
    }

    /**
     * @skip
     */
    static String _String(int[] codePoints, int offset, int count) {
        char[] chars = new char[count * 2];
        int charIdx = 0;
        while (count-- > 0) {
            charIdx += Character.toChars(codePoints[offset++], chars, charIdx);
        }
        return valueOf(chars, 0, charIdx);
    }

    /**
     * @skip
     */
    static String _String(String other) {
        return other;
    }

    /**
     * @skip
     */
    static String _String(StringBuffer sb) {
        return valueOf(sb);
    }

    /**
     * @skip
     */
    static String _String(StringBuilder sb) {
        return valueOf(sb);
    }

    private static native boolean __equals(String me, Object other) /*-{
        // Coerce me to a primitive string to force string comparison
        return String(me) == other;
    }-*/;

    // CHECKSTYLE_ON

    private static native int compareTo(String thisStr, String otherStr) /*-{
        // Coerce to a primitive string to force string comparison
        thisStr = String(thisStr);
        if (thisStr == otherStr) {
            return 0;
        }
        return thisStr < otherStr ? -1 : 1;
    }-*/;

    /**
     * Encode a single character in UTF8.
     *
     * @param bytes byte array to store character in
     * @param ofs offset into byte array to store first byte
     * @param codePoint character to encode
     * @return number of bytes consumed by encoding the character
     * @throws IllegalArgumentException if codepoint >= 2^26
     */
    private static int encodeUtf8(byte[] bytes, int ofs, int codePoint) {
        if (codePoint < (1 << 7)) {
            bytes[ofs] = (byte) (codePoint & 127);
            return 1;
        } else if (codePoint < (1 << 11)) {
            // 110xxxxx 10xxxxxx
            bytes[ofs++] = (byte) (((codePoint >> 6) & 31) | 0xC0);
            bytes[ofs] = (byte) ((codePoint & 63) | 0x80);
            return 2;
        } else if (codePoint < (1 << 16)) {
            // 1110xxxx 10xxxxxx 10xxxxxx
            bytes[ofs++] = (byte) (((codePoint >> 12) & 15) | 0xE0);
            bytes[ofs++] = (byte) (((codePoint >> 6) & 63) | 0x80);
            bytes[ofs] = (byte) ((codePoint & 63) | 0x80);
            return 3;
        } else if (codePoint < (1 << 21)) {
            // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
            bytes[ofs++] = (byte) (((codePoint >> 18) & 7) | 0xF0);
            bytes[ofs++] = (byte) (((codePoint >> 12) & 63) | 0x80);
            bytes[ofs++] = (byte) (((codePoint >> 6) & 63) | 0x80);
            bytes[ofs] = (byte) ((codePoint & 63) | 0x80);
            return 4;
        } else if (codePoint < (1 << 26)) {
            // 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
            bytes[ofs++] = (byte) (((codePoint >> 24) & 3) | 0xF8);
            bytes[ofs++] = (byte) (((codePoint >> 18) & 63) | 0x80);
            bytes[ofs++] = (byte) (((codePoint >> 12) & 63) | 0x80);
            bytes[ofs++] = (byte) (((codePoint >> 6) & 63) | 0x80);
            bytes[ofs] = (byte) ((codePoint & 63) | 0x80);
            return 5;
        }
        throw new IllegalArgumentException("Character out of range: " + codePoint);
    }

    private static native String fromCharCode(char ch) /*-{
        return String.fromCharCode(ch);
    }-*/;

    private static String fromCodePoint(int codePoint) {
        if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            char hiSurrogate = Character.getHighSurrogate(codePoint);
            char loSurrogate = Character.getLowSurrogate(codePoint);
            return String.fromCharCode(hiSurrogate)
                    + String.fromCharCode(loSurrogate);
        } else {
            return String.fromCharCode((char) codePoint);
        }
    }

    private static byte[] getBytesLatin1(String str) {
        int n = str.length();
        byte[] bytes = new byte[n];
        for (int i = 0; i < n; ++i) {
            bytes[i] = (byte) (str.charAt(i) & 255);
        }
        return bytes;
    }

    private static byte[] getBytesUtf8(String str) {
        // TODO(jat): consider using unescape(encodeURIComponent(bytes)) instead
        int n = str.length();
        int byteCount = 0;
        for (int i = 0; i < n; ) {
            int ch = str.codePointAt(i);
            i += Character.charCount(ch);
            if (ch < (1 << 7)) {
                byteCount++;
            } else if (ch < (1 << 11)) {
                byteCount += 2;
            } else if (ch < (1 << 16)) {
                byteCount += 3;
            } else if (ch < (1 << 21)) {
                byteCount += 4;
            } else if (ch < (1 << 26)) {
                byteCount += 5;
            }
        }
        byte[] bytes = new byte[byteCount];
        int out = 0;
        for (int i = 0; i < n; ) {
            int ch = str.codePointAt(i);
            i += Character.charCount(ch);
            out += encodeUtf8(bytes, out, ch);
        }
        return bytes;
    }

    private static String latin1ToString(byte[] bytes, int ofs, int len) {
        char[] chars = new char[len];
        for (int i = 0; i < len; ++i) {
            chars[i] = (char) (bytes[ofs + i] & 255);
        }
        return valueOf(chars);
    }

    private static native boolean regionMatches(String thisStr,
                                                boolean ignoreCase, int toffset, String other, int ooffset, int len) /*-{
        if (toffset < 0 || ooffset < 0 || len <= 0) {
            return false;
        }

        if (toffset + len > thisStr.length || ooffset + len > other.length) {
            return false;
        }

        var left = thisStr.substr(toffset, len);
        var right = other.substr(ooffset, len);

        if (ignoreCase) {
            left = left.toLowerCase();
            right = right.toLowerCase();
        }

        return left == right;
    }-*/;

    private static String utf8ToString(byte[] bytes, int ofs, int len) {
        // TODO(jat): consider using decodeURIComponent(escape(bytes)) instead
        int charCount = 0;
        for (int i = 0; i < len; ) {
            ++charCount;
            byte ch = bytes[ofs + i];
            if ((ch & 0xC0) == 0x80) {
                throw new IllegalArgumentException("Invalid UTF8 sequence");
            } else if ((ch & 0x80) == 0) {
                ++i;
            } else if ((ch & 0xE0) == 0xC0) {
                i += 2;
            } else if ((ch & 0xF0) == 0xE0) {
                i += 3;
            } else if ((ch & 0xF8) == 0xF0) {
                i += 4;
            } else {
                // no 5+ byte sequences since max codepoint is less than 2^21
                throw new IllegalArgumentException("Invalid UTF8 sequence");
            }
            if (i > len) {
                throw new IndexOutOfBoundsException("Invalid UTF8 sequence");
            }
        }
        char[] chars = new char[charCount];
        int outIdx = 0;
        int count = 0;
        for (int i = 0; i < len; ) {
            int ch = bytes[ofs + i++];
            if ((ch & 0x80) == 0) {
                count = 1;
                ch &= 127;
            } else if ((ch & 0xE0) == 0xC0) {
                count = 2;
                ch &= 31;
            } else if ((ch & 0xF0) == 0xE0) {
                count = 3;
                ch &= 15;
            } else if ((ch & 0xF8) == 0xF0) {
                count = 4;
                ch &= 7;
            } else if ((ch & 0xFC) == 0xF8) {
                count = 5;
                ch &= 3;
            }
            while (--count > 0) {
                byte b = bytes[ofs + i++];
                if ((b & 0xC0) != 0x80) {
                    throw new IllegalArgumentException("Invalid UTF8 sequence at "
                            + (ofs + i - 1) + ", byte=" + Integer.toHexString(b));
                }
                ch = (ch << 6) | (b & 63);
            }
            outIdx += Character.toChars(ch, chars, outIdx);
        }
        return valueOf(chars);
    }

    public String() {
        // magic delegation to _String
        _String();
    }

    public String(byte[] bytes) {
        // magic delegation to _String
        _String(bytes);
    }

    public String(byte[] bytes, int ofs, int len) {
        // magic delegation to _String
        _String(bytes, ofs, len);
    }

    public String(byte[] bytes, int ofs, int len, String charsetName)
            throws UnsupportedEncodingException {
        // magic delegation to _String
        _String(bytes, ofs, len, charsetName);
    }

    public String(byte[] bytes, String charsetName)
            throws UnsupportedEncodingException {
        // magic delegation to _String
        _String(bytes, charsetName);
    }

    public String(char value[]) {
        // magic delegation to _String
        _String(value);
    }

    public String(char value[], int offset, int count) {
        // magic delegation to _String
        _String(value, offset, count);
    }

    public String(int codePoints[], int offset, int count) {
        // magic delegation to _String
        _String(codePoints, offset, count);
    }

    public String(String other) {
        // magic delegation to _String
        _String(other);
    }

    public String(StringBuffer sb) {
        // magic delegation to _String
        _String(sb);
    }

    public String(StringBuilder sb) {
        // magic delegation to _String
        _String(sb);
    }

    public native char charAt(int index) /*-{
        return this.charCodeAt(index);
    }-*/;

    public int codePointAt(int index) {
        return Character.codePointAt(this, index, length());
    }

    public int codePointBefore(int index) {
        return Character.codePointBefore(this, index, 0);
    }

    public int codePointCount(int beginIndex, int endIndex) {
        return Character.codePointCount(this, beginIndex, endIndex);
    }

    public int compareTo(String other) {
        return compareTo(this, other);
    }

    public int compareToIgnoreCase(String other) {
        return compareTo(toLowerCase(), other.toLowerCase());
    }

    public native String concat(String str) /*-{
        return this + str;
    }-*/;

    public boolean contains(CharSequence s) {
        return indexOf(s.toString()) != -1;
    }

    public boolean contentEquals(CharSequence cs) {
        return equals(cs.toString());
    }

    public boolean contentEquals(StringBuffer sb) {
        return equals(sb.toString());
    }

    public native boolean endsWith(String suffix) /*-{
        return (this.lastIndexOf(suffix) != -1)
                && (this.lastIndexOf(suffix) == (this.length - suffix.length));
    }-*/;

    @Override
    public boolean equals(Object other) {
        if (!(other instanceof String)) {
            return false;
        }
        return __equals(this, other);
    }

    public native boolean equalsIgnoreCase(String other) /*-{
        if (other == null)
            return false;
        return (this == other) || (this.toLowerCase() == other.toLowerCase());
    }-*/;

    public byte[] getBytes() {
        // default character set for GWT is UTF-8
        return getBytesUtf8(this);
    }

    public byte[] getBytes(String charSet) throws UnsupportedEncodingException {
        if (CHARSET_UTF8.equals(charSet)) {
            return getBytesUtf8(this);
        }
        if (CHARSET_8859_1.equals(charSet) || CHARSET_LATIN1.equals(charSet)) {
            return getBytesLatin1(this);
        }
        throw new UnsupportedEncodingException(charSet + " is not supported");
    }

    public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) {
        for (int srcIdx = srcBegin; srcIdx < srcEnd; ++srcIdx) {
            dst[dstBegin++] = charAt(srcIdx);
        }
    }

    @Override
    public int hashCode() {
        return HashCache.getHashCode(this);
    }

    public int indexOf(int codePoint) {
        return indexOf(fromCodePoint(codePoint));
    }

    public int indexOf(int codePoint, int startIndex) {
        return this.indexOf(String.fromCodePoint(codePoint), startIndex);
    }

    public native int indexOf(String str) /*-{
        return this.indexOf(str);
    }-*/;

    public native int indexOf(String str, int startIndex) /*-{
        return this.indexOf(str, startIndex);
    }-*/;

    public native String intern() /*-{
        return String(this);
    }-*/;

    public native boolean isEmpty() /*-{
        return !this.length;
    }-*/;

    public int lastIndexOf(int codePoint) {
        return lastIndexOf(fromCodePoint(codePoint));
    }

    public int lastIndexOf(int codePoint, int startIndex) {
        return lastIndexOf(fromCodePoint(codePoint), startIndex);
    }

    public native int lastIndexOf(String str) /*-{
        return this.lastIndexOf(str);
    }-*/;

    public native int lastIndexOf(String str, int start) /*-{
        return this.lastIndexOf(str, start);
    }-*/;

    public native int length() /*-{
        return this.length;
    }-*/;

    /**
     * Regular expressions vary from the standard implementation. The
     * <code>regex</code> parameter is interpreted by JavaScript as a JavaScript
     * regular expression. For consistency, use only the subset of regular
     * expression syntax common to both Java and JavaScript.
     *
     * TODO(jat): properly handle Java regex syntax
     */
    public native boolean matches(String regex) /*-{
        var matchObj = new RegExp(regex).exec(this);
        // if there is no match at all, matchObj will be null
        // matchObj[0] is the entire matched string
        return (matchObj == null) ? false : (this == matchObj[0]);
    }-*/;

    public int offsetByCodePoints(int index, int codePointOffset) {
        return Character.offsetByCodePoints(this, index, codePointOffset);
    }

    public boolean regionMatches(boolean ignoreCase, int toffset, String other,
                                 int ooffset, int len) {
        if (other == null) {
            throw new NullPointerException();
        }
        return regionMatches(this, ignoreCase, toffset, other, ooffset, len);
    }

    public boolean regionMatches(int toffset, String other, int ooffset, int len) {
        if (other == null) {
            throw new NullPointerException();
        }
        return regionMatches(this, false, toffset, other, ooffset, len);
    }

    public native String replace(char from, char to) /*-{
        // We previously used \\uXXXX, but Safari 2 doesn't match them properly in RegExp
        // See http://bugs.webkit.org/show_bug.cgi?id=8043
        //     http://bugs.webkit.org/show_bug.cgi?id=6257
        //     http://bugs.webkit.org/show_bug.cgi?id=7253
        var regex;
        if (from < 256) {
            regex = @java.lang.Integer::toHexString(I)(from);
            regex = '\\x' + "00".substring(regex.length) + regex;
        } else {
            // this works because characters above 255 can't be regex special chars
            regex = String.fromCharCode(from);
        }
        return this.replace(new RegExp(regex, "g"), String.fromCharCode(to));
    }-*/;

    public String replace(CharSequence from, CharSequence to) {
        // Implementation note: This uses a regex replacement instead of
        // a string literal replacement because Safari does not
        // follow the spec for "$$" in the replacement string: it
        // will insert a literal "$$". IE and Firefox, meanwhile,
        // treat "$$" as "$".

        // Escape regex special characters from literal replacement string.
        String regex = from.toString().replaceAll("([/\\\\\\.\\*\\+\\?\\|\\(\\)\\[\\]\\{\\}$^])", "\\\\$1");
        // Escape $ since it is for match backrefs and \ since it is used to escape
        // $.
        String replacement = to.toString().replaceAll("\\\\", "\\\\\\\\").replaceAll("\\$", "\\\\$");

        return replaceAll(regex, replacement);
    }

    /**
     * Regular expressions vary from the standard implementation. The
     * <code>regex</code> parameter is interpreted by JavaScript as a JavaScript
     * regular expression. For consistency, use only the subset of regular
     * expression syntax common to both Java and JavaScript.
     *
     * TODO(jat): properly handle Java regex syntax
     */
    public native String replaceAll(String regex, String replace) /*-{
        replace = @java.lang.String::__translateReplaceString(Ljava/lang/String;)(replace);
        return this.replace(new RegExp(regex, "g"), replace);
    }-*/;

    /**
     * Regular expressions vary from the standard implementation. The
     * <code>regex</code> parameter is interpreted by JavaScript as a JavaScript
     * regular expression. For consistency, use only the subset of regular
     * expression syntax common to both Java and JavaScript.
     *
     * TODO(jat): properly handle Java regex syntax
     */
    public native String replaceFirst(String regex, String replace) /*-{
        replace = @java.lang.String::__translateReplaceString(Ljava/lang/String;)(replace);
        return this.replace(new RegExp(regex), replace);
    }-*/;

    /**
     * Regular expressions vary from the standard implementation. The
     * <code>regex</code> parameter is interpreted by JavaScript as a JavaScript
     * regular expression. For consistency, use only the subset of regular
     * expression syntax common to both Java and JavaScript.
     */
    public String[] split(String regex) {
        return split(regex, 0);
    }

    /**
     * Regular expressions vary from the standard implementation. The
     * <code>regex</code> parameter is interpreted by JavaScript as a JavaScript
     * regular expression. For consistency, use only the subset of regular
     * expression syntax common to both Java and JavaScript.
     *
     * TODO(jat): properly handle Java regex syntax
     */
    public native String[] split(String regex, int maxMatch) /*-{
        // The compiled regular expression created from the string
        var compiled = new RegExp(regex, "g");
        // the Javascipt array to hold the matches prior to conversion
        var out = [];
        // how many matches performed so far
        var count = 0;
        // The current string that is being matched; trimmed as each piece matches
        var trail = this;
        // used to detect repeated zero length matches
        // Must be null to start with because the first match of "" makes no
        // progress by intention
        var lastTrail = null;
        // We do the split manually to avoid Javascript incompatibility
        while (true) {
            // None of the information in the match returned are useful as we have no
            // subgroup handling
            var matchObj = compiled.exec(trail);
            if (matchObj == null || trail == "" ||
                    (count == (maxMatch - 1) && maxMatch > 0)) {
                out[count] = trail;
                break;
            } else {
                out[count] = trail.substring(0,matchObj.index);
                trail = trail.substring(matchObj.index + matchObj[0].length, trail.length);
                // Force the compiled pattern to reset internal state
                compiled.lastIndex = 0;
                // Only one zero length match per character to ensure termination
                if (lastTrail == trail) {
                    out[count] = trail.substring(0,1);
                    trail = trail.substring(1);
                }
                lastTrail = trail;
                count++;
            }
        }
        // all blank delimiters at the end are supposed to disappear if maxMatch == 0;
        // however, if the input string is empty, the output should consist of a
        // single empty string
        if (maxMatch == 0 && this.length > 0) {
            var lastNonEmpty = out.length;
            while (lastNonEmpty > 0 && out[lastNonEmpty - 1] == "") {
                --lastNonEmpty;
            }
            if (lastNonEmpty < out.length) {
                out.splice(lastNonEmpty, out.length - lastNonEmpty);
            }
        }
        var jr = @java.lang.String::__createArray(I)(out.length);
        for(var i = 0; i < out.length; ++i) {
            jr[i] = out[i];
        }
        return jr;
    }-*/;

    public boolean startsWith(String prefix) {
        return indexOf(prefix) == 0;
    }

    public boolean startsWith(String prefix, int toffset) {
        if (toffset < 0 || toffset >= length()) {
            return false;
        } else {
            return indexOf(prefix, toffset) == toffset;
        }
    }

    public CharSequence subSequence(int beginIndex, int endIndex) {
        return this.substring(beginIndex, endIndex);
    }

    public native String substring(int beginIndex) /*-{
        return this.substr(beginIndex, this.length - beginIndex);
    }-*/;

    public native String substring(int beginIndex, int endIndex) /*-{
        return this.substr(beginIndex, endIndex - beginIndex);
    }-*/;

    public char[] toCharArray() {
        int n = this.length();
        char[] charArr = new char[n];
        getChars(0, n, charArr, 0);
        return charArr;
    }

    public native String toLowerCase() /*-{
        return this.toLowerCase();
    }-*/;

    @Override
    public String toString() {
        return this;
    }

    public native String toUpperCase() /*-{
        return this.toUpperCase();
    }-*/;

    public native String trim() /*-{
        if (this.length == 0 || (this.charAt(0) > '\u0020' && this.charAt(this.length-1) > '\u0020')) {
            return this;
        }
        var r1 = this.replace(/^(\s*)/, '');
        var r2 = r1.replace(/\s*$/, '');
        return r2;
    }-*/;
}